#coding = utf-8

'''
程 序 名：墨迹天气爬虫
编 写 人：bxgj
运行环境：win7x64 Python3.6.4
修改日志：2018.09.09 新建
          2018.09.10 完成爬取主要天气信息功能
          2018.09.12 完成爬取风力等信息的功能
          2018.09.14 完成爬取空气质量信息的功能
          2018.09.15 重构代码
          2018.09.16 完成数据保存功能
          2018.09.21 功能基本完成，修复部分细节问题
          2020.05.07 更加合理的一次大范围重构重构，基本上整个代码结构都变了，更适合作为模块导入
版    本：V2.0
备    注：由于最近没有极端天气，所以爬取高温预警、雷电预警之类的功能暂未完成

空气质量评定标准，AQI分级
优 0-50
良 51-100
轻度污染 101-150
中度污染 151-200
重度污染 201-300
严重污染 301-500
500以上爆表了


'''

# ---------------- 导入模块 ----------------
from bs4 import BeautifulSoup
import requests
import time
import datetime
import os
import re
import csv


# ---------------- 全局变量、初始化等 ----------------

# 要爬取的链接
wea_url = "https://tianqi.moji.com/weather/china/shaanxi/yanta-district"
aqi_url = "https://tianqi.moji.com/aqi/china/shaanxi/yanta-district"

'''
墨迹天气官方网站
https://tianqi.moji.com/

链接格式
天气预报 https://tianqi.moji.com/weather/china/省会/市、区、县等
空气质量 https://tianqi.moji.com/aqi/china/省会/市、区、县等

地级市，直接用拼音，如
https://tianqi.moji.com/weather/china/shaanxi/xian

市区，区名的拼音加-district，如
https://tianqi.moji.com/weather/china/shaanxi/yanta-district

县，县名的拼音加-county
镇，镇名的拼音加-town

其他的地名，如XX山、XX风景区、XX湖基本都是拼音加英文

特殊地名特殊处理，如
秦始皇陵
mausoleum-of-the-first-qin-emperor
陕西历史博物馆
shanxi-history-museum

'''


# 是否使用调试模式
# DEBUG_MODE = True
DEBUG_MODE = False

# 是否保存网页文件
# DUMP_HTML = True
DUMP_HTML = False


# ---------------- 类定义 ----------------

class WeatherSpider():
    def __init__(self, aqi_url="", wea_url=""):
        self.wea_url = wea_url
        self.aqi_url = aqi_url

        # 保存爬取后的网页文本
        self.__aqi_html = ""
        self.__wea_html = ""

        # 爬取时间
        crawltime = datetime.datetime.now()
        self.crawltime = crawltime.replace(microsecond=0)

        self.rt_wea = {
            # 天气基本信息
            "uptime" : None, # 这个时间指的是网站上显示的更新时间，不是爬取的时间，类型是class datetime
            "city"   : "",
            "weather": "",
            "temp"   : None,

            # 天气详细信息
            "humi"       : -1,
            "wind_dir"   : "",
            "wind_min"   : -1,
            "wind_max"   : -1,
            "tips"       : "",
            "description": "", # 总体天气描述

            # 天气其他信息，比如高温预警、雷电预警之类的
            "other_info": "",
        }

        self.rt_aqi = {
            # 基本信息
            "uptime" : None, # 这个时间指的是网站上显示的更新时间，不是爬取的时间，类型是class datetime
            "city"   : "",

            # 空气质量
            "aqi"      : -1,
            "aqi_level": "",
            "aqi_PM10" : -1,
            "aqi_PM2P5": -1,
            "aqi_NO2"  : -1,
            "aqi_SO2"  : -1,
            "aqi_O3"   : -1,
            "aqi_CO"   : -1,
        }

        # 未来几天的天气预报信息都由这个字典组成数组
        __future_wea = {
            # 基本信息
            "uptime" : None, # 这个时间指的是网站上显示的更新时间，不是爬取的时间，类型是class datetime
            "city"   : "",

            # 天气基本信息
            "weather" : "",
            "temp_min": None,
            "temp_max": None,

            # 天气详细信息
            "wind_dir": "",
            "wind_min": -1,
            "wind_max": -1,

            # 空气质量
            "aqi"      : -1,
            "aqi_level": "",
            
            # 天气其他信息，比如高温预警、雷电预警之类的
            "other_info": "",
        }

        # 未来几天的天气预报信息都由 __future_wea 组成数组
        self.forecast_wea = [__future_wea for i in range(3)]

    def __request_web(self, type=""):
        url = ""
        if (type == "aqi"):
            url = self.aqi_url
        elif (type == "wea"):
            url = self.wea_url
        else:
            print("参数有误")
            return -1

        if (DEBUG_MODE):
            # 本地测试文件
            with open(type + ".html", 'r', encoding='utf-8') as html_file:
                html_text = html_file.read()
            if (type == "aqi"):
                self.__aqi_html = html_text
            elif (type == "wea"):
                self.__wea_html = html_text
        else:
            try:
                web_data = requests.get(url)
            except requests.exceptions.ConnectionError:
                print("网络连接异常")
                return -1
            except Exception:
                print("其他异常")
                return -2

            if (web_data.status_code != requests.codes.ok):
                print("服务器响应异常", web_data.status_code)
                return -3

            if (DUMP_HTML):
                dump_path = ""

                this_path = os.path.realpath(__file__)
                dir_path = os.path.dirname(this_path)
                dump_path = os.path.join(dir_path, "dump_web_data")

                if not (os.path.exists(dump_path)):
                    os.mkdir(dump_path)

                # strftime()格式化输出时间，避免文件名有特殊符号
                tmpfilename = self.crawltime.strftime("%Y%m%d_%H%M%S_")+type+".html"
                dump_filename = os.path.join(dump_path, tmpfilename)
                
                with open(dump_filename, 'w', encoding='utf-8') as html_file:
                    html_file.write(web_data.text)
                    html_file.flush() # 立即刷新写入硬盘，避免程序崩了数据也没了
            
            if (type == "aqi"):
                self.__aqi_html = web_data.text
            elif (type == "wea"):
                self.__wea_html = web_data.text

    # 解析html
    def __analyse_rt_aqi(self):
        soup = BeautifulSoup(self.__aqi_html, "lxml")

        # 通过id查找空气质量和等级
        aqi_value_tag = soup.select("#aqi_value")
        # print("空气质量", aqi_value_tag[0].get_text())
        self.rt_aqi["aqi"] = int(aqi_value_tag[0].get_text())

        aqi_desc_tag = soup.select("#aqi_desc")
        # print("等级", aqi_desc_tag[0].get_text())
        self.rt_aqi["aqi_level"] = aqi_desc_tag[0].get_text()

        # 爬取空气质量详情
        # 这个标签之间是空气质量详情<ul class="clearfix">
        aqi_info_item = soup.find("ul", class_="clearfix")

        # 获取空气质量条目的名称
        aqi_info_name_list = []
        aqi_info_name_tag = aqi_info_item.find_all("em")
        for name in aqi_info_name_tag:
            # join方法用指定字符串连接元组或列表，"指定字符串".join(元组或列表)
            aqi_info_name_list.append("".join(name.strings))

        # 获取空气质量条目的数值
        aqi_info_value_list = []
        aqi_info_value_tag = aqi_info_item.find_all("span")
        for value in aqi_info_value_tag:
            # aqi_info_value_list.append(int(value.string))
            aqi_info_value_list.append(int(float(value.string)))

        # 空气质量合并为字典
        aqi_info_list = dict(zip(aqi_info_name_list, aqi_info_value_list))
        # print(aqi_info_list)

        # 保存
        self.rt_aqi["aqi_PM10"] = aqi_info_list["PM10"]
        self.rt_aqi["aqi_PM2P5"] = aqi_info_list["PM2.5"]
        self.rt_aqi["aqi_NO2"] = aqi_info_list["NO2"]
        self.rt_aqi["aqi_SO2"] = aqi_info_list["SO2"]
        self.rt_aqi["aqi_O3"] = aqi_info_list["O3"]
        self.rt_aqi["aqi_CO"] = aqi_info_list["CO"]

        # 发布时间
        # 这个发布时间我感觉严重滞后，就不用了
        # aqi_info_time_raw = soup.find(class_="aqi_info_time")
        # aqi_info_time = aqi_info_time_raw.b.string
        # # print(aqi_info_time)
        # uptime = re.search(r"(\d+)年(\d+)月(\d+)日 (\d+)时(\d+)分", aqi_info_time).groups()
        # print(uptime)

        # print("---------------- 结束3 ----------------")
        
        pass

    def __analyse_rt_wea(self):
        soup = BeautifulSoup(self.__wea_html, "lxml")

        # 城市信息
        # city_tag = soup.find("div", class_="search_default")
        city_tag = soup.select(".search_default > em")
        city_name = city_tag[0].get_text().replace(' ', '')
        # city_name = "".join(city_name.split()) # 先分片，再合并是一种去掉包括\xa0等特殊空白字符的方法
        self.rt_wea["city"] = city_name # 保存城市信息

        # 发布时间
        uptime_tag = soup.find("strong", class_="info_uptime")
        uptime_str = uptime_tag.get_text()
        uptime_str = re.search(r"(\d+):(\d+)", uptime_str).groups() # 用正则提取时分
        tmp_datetime = datetime.datetime.now()
        uptime = tmp_datetime.replace(hour=int(uptime_str[0]), minute=int(uptime_str[1]),
                                        second=0, microsecond=0)
        self.rt_wea["uptime"] = uptime

        # 获取描述信息
        # 通过属性的值来查找这个标签 <meta name="description" content="这里是天气概要">
        description_tag = soup.select('meta[name="description"]')

        # 用tag的get方法获取指定属性的值
        weather_description = description_tag[0].get("content")
        # weather_description = description_tag[0]["content"] # 这种写法也是可以的
        weather_description.replace(" ", "").replace(",", "，") # 清除多余空格，替换英文标点符号
        self.rt_wea["description"] = weather_description


        # 抓取实时天气信息
        tmp_tag = soup.find("div", class_="wea_weather clearfix")
        # temp_tag = tmp_tag.find("em")
        # self.rt_wea["temp"] = float(temp_tag.string)
        # wea_tag = tmp_tag.find("b")
        # self.rt_wea["weather"] = wea_tag.string
        # 上面这4行的写法比较罗嗦
        self.rt_wea["temp"] = float(tmp_tag.em.string) # em标签中是温度
        self.rt_wea["weather"] = tmp_tag.b.string # b标签中是天气

        # 这个标签下是湿度和风力信息
        tmp_tag = soup.find("div", class_="wea_about clearfix")

        humi_str = tmp_tag.span.string # span标签下是湿度
        humi_value = re.search(r"(\d+)", humi_str).group() 
        self.rt_wea["humi"] = int(humi_value)

        wind_str = tmp_tag.em.string # em标签下是风力
        try:
            wind_dir = re.search(r"([东西南北微无台]+风)", wind_str).group()
        except AttributeError:
            print("可能是风力等级有新的汉字，原始数据：", wind_str)
        else:
            self.rt_wea["wind_dir"] = wind_dir

        wind_min = re.search(r"(\d+)", wind_str).group()
        self.rt_wea["wind_min"] = wind_min

        tmp_tag = soup.find("div", class_="wea_tips clearfix")
        # tips_tag = tmp_tag.find("em")
        self.rt_wea["tips"] = tmp_tag.em.string 

        # 与这种方式等价
        # tmp_tag = soup.select("div.wea_tips.clearfix > em")
        # print(tmp_tag[0].get_text())


    def __analyse_forecast_wea(self):
        soup = BeautifulSoup(self.__wea_html, "lxml")

        # 3天预报爬虫
        tmp_tag = soup.find_all("ul", class_ = "days clearfix")
        day_num = 0
        for oneday in tmp_tag:
            items_tag = oneday.find_all("li")

            # 城市和更新时间前面已经爬了，这里直接用
            self.forecast_wea[day_num]["uptime"] = self.rt_wea["uptime"]
            self.forecast_wea[day_num]["city"] = self.rt_wea["city"]

            # 1 天气
            self.forecast_wea[day_num]["weather"] = re.sub(r"\s", "", items_tag[1].get_text())
            
            # 2 温度
            temps = re.findall(r"(\d+)", items_tag[2].get_text())
            self.forecast_wea[day_num]["temp_min"] = int(temps[0])
            self.forecast_wea[day_num]["temp_max"] = int(temps[1])
            
            # 3 风力
            self.forecast_wea[day_num]["wind_dir"] = items_tag[3].em.get_text() # 风向
            wind_values = re.findall(r"(\d+)", items_tag[3].b.get_text())  # 风力
            if (len(wind_values) == 1):
                self.forecast_wea[day_num]["wind_min"] = int(wind_values[0])
            elif (len(wind_values) == 2):
                self.forecast_wea[day_num]["wind_min"] = int(wind_values[0])
                self.forecast_wea[day_num]["wind_max"] = int(wind_values[1])
            else:
                print("解析风力级数出错")

            # 4 空气质量
            aqi_str = re.sub(r"\s", "", items_tag[4].get_text())
            self.forecast_wea[day_num]["aqi"] = int(re.search(r"(\d+)", aqi_str).group())
            self.forecast_wea[day_num]["aqi_level"] = re.search(r"(\D+)", aqi_str).group()

            day_num += 1

    
    def update(self):
        if (len(self.aqi_url) == 0):
            print("aqi_url not set!")
            return -1
        if (len(self.wea_url) == 0):
            print("wea_url not set!")
            return -1
        self.__request_web("aqi")
        self.__request_web("wea")
        self.__analyse_rt_aqi()
        self.__analyse_rt_wea()
        self.__analyse_forecast_wea()


def main():
    print("---------------- test ----------------")
    wea_data = WeatherSpider(aqi_url=aqi_url, wea_url=wea_url)
    wea_data.update()
    print(wea_data.rt_aqi)
    print(wea_data.rt_wea)
    print(wea_data.forecast_wea)
    print("---------------- end ----------------")


# ---------------- 程序入口 ----------------
if (__name__ == "__main__"):
    main()
